﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NF.Core.SOM
{
    public struct MapNode
    {
        public NPoint W;
        public float X, Y;
    }

    public class SelfOrganizingMap
    {
        public void Build(int gridCols, int gridRows, IEnumerable<NPoint> points)
        {
            mPointOrder = points.First().Order;
            foreach (var p in points)
            {
                if (p.Order != mPointOrder)
                    throw new ArgumentException("points");
            }

            InitGrid(gridCols, gridRows, points);

            mPoints = points;
        }

        public void Iterate(int iterations)
        {
            double t = 0.0;
            double d = 1.0 / (double)iterations;

            for (int i = 0; i < iterations; ++i, t += d)
            {
                Iteration(t);
            }
        }

        public void Iteration(double t)
        {
            foreach (var p in mPoints)
                FeedPoint(p, t);
        }

        private void FeedPoint(NPoint pt, double t)
        {
            int bmuIdx = FindBMUIndex(pt);
            double s;

            for (int i = 0; i < mMap.Length; ++i)
            {
                s = LearningCoefficient(t) * NeighborhoodFunction(t, ref mMap[i], ref mMap[bmuIdx]);
                if (Math.Abs(s) < 0.0001)
                    continue;

                for (int k = 0; k < mPointOrder; ++k)
                    mMap[i].W[k] += s * (pt[k] - mMap[i].W[k]);
            }
        }

        private int FindBMUIndex(NPoint pt)
        {
            double d;
            double minDist = double.MaxValue;
            int minDistIdx = -1;

            for (int i = 0; i < mMap.Length; ++i)
            {
                d = NPoint.Distance(mMap[i].W, pt);
                if (d < minDist)
                {
                    minDist = d;
                    minDistIdx = i;
                }
            }

            return minDistIdx;
        }

        private static double LearningCoefficient(double t)
        {
            return Math.Max(1.0 - t, 0.0);
        }

        private double NeighborhoodFunction(double t, ref MapNode a, ref MapNode b)
        {
            double dx = a.X - b.X;
            double dy = a.Y - b.Y;

            double d = Math.Sqrt((dx * dx) + (dy * dy));

            t = Math.Max(1.0 - t, 0.0);
            d = Math.Max((mMapRadius - d + 1.0), 0.0) / mMapRadius;

            return d * t;
        }

        private void InitGrid(int gridCols, int gridRows, IEnumerable<NPoint> points)
        {
            mGridCols = gridCols;
            mGridRows = gridRows;

            mMap = new MapNode[mGridRows * mGridCols];

            NPoint min = new NPoint(mPointOrder, double.MaxValue);
            NPoint max = new NPoint(mPointOrder, double.MinValue);

            foreach (var p in points)
            {
                for (int i = 0; i < mPointOrder; ++i)
                {
                    if (p[i] < min[i])
                        min[i] = p[i];
                    if (p[i] > max[i])
                        max[i] = p[i];
                }
            }

            int idx;
            double s = 0.0, t = 0.0;
            double ds = 1.0 / (double)(gridCols - 1);
            double dt = 1.0 / (double)(gridRows - 1);

            for (int j = 0; j < gridRows; ++j, t += dt)
            {
                for (int i = 0; i < gridCols; ++i, s += ds)
                {
                    idx = i + j * gridRows;
                    mMap[idx].X = i;
                    mMap[idx].Y = j;
                    mMap[idx].W = new NPoint(mPointOrder);

                    for (int k = 0; k < mPointOrder; ++k)
                    {
                        mMap[idx].W[k] = min[k] + max[k] * s * t;
                    }
                }
            }

            mMapRadius = (double)Math.Min(mGridCols, mGridRows) * 0.5;
        }

        public int GridCols
        {
            get { return mGridCols; }
        }

        public int GridRows
        {
            get { return mGridRows; }
        }

        public MapNode GetMapNode(int i, int j)
        {
            return mMap[i + j * mGridCols];
        }

        private double mMapRadius;
        private IEnumerable<NPoint> mPoints;
        private int mPointOrder;
        private int mGridCols, mGridRows;
        private MapNode[] mMap;                                                     
    }
}
